組込み現場の「C++」プログラミング 明日から使える徹底入門

高木 信尚(株式会社クローバーフィールド

1.4 Cの資産を活用しよう

C++を使うといっても,OSやミドルウェア,あるいは社内の過去の資産といった類は,Cで実装されている場合が多く,多かれ少なかれそれらを活用する必要があるかと思います.C++の大きな特長の1つとして,Cで書かれた資産を,ほとんどそのまま利用できる点があります.しかも,Cで書かれたソースコードをC++としてコンパイルし直すのではなく,Cでコンパイルしたままリンクすることができるのです.

1.4.1 Cの資産を利用するためのテクニック

Cの資産の利用を考えるとき,実装に使用されたCのバージョンが重要になってきます.標準C++は,ISO/IEC 9899:1990とISO/IEC 9899/AMD1:1995に対して,おおむね上位互換になってはいますが,Cの現行規格であるISO/IEC 9899:1999(通称C99)では,C++には存在しない文法拡張も多く,場合によっては簡単に再利用することができません.しかし,現在普及しているCコンパイラの多くは,まだC99には対応しておらず,また,C99に対応していたとしても,同じコンパイラベンダーのC++コンパイラであれば,C99の拡張を利用できることも多いため,ここではC99については特に考慮しないことにします.

C99のことを考慮しなければ,Cの資産を利用するためには,関数などの宣言を行うヘッダファイルを工夫するだけでかまいません.工夫すべきポイントを次に挙げてみます.

  • 外部識別子をすべてC結合にする.
  • C++のキーワードや予約名は使用しない.
  • CとC++の非互換性に注意する.

それでは,これらの工夫すべき点を順に解説していきます.

外部識別子をすべてC結合にする

C++では,関数の多重定義を行うことができます.多重定義された関数を解決するには,引数の型情報を何らかの形で反映したシンボルをリンカに渡すことになります.また,処理系によっては,関数だけでなく,オブジェクトのシンボルにも型情報が反映されます.Cでは,関数やオブジェクトの型情報がシンボルに埋め込まれることはありませんので,この問題を解決するには,C++側でその外部識別子が「C結合」であることを明示的に指定する必要があります.C結合であることを指定するには,次の2つの例のように,extern "C"を記述する必要があります.

extern "C" void f();
extern "C" {
    void g();
    void h();
}

これは,通常のextern指定子のように,個々の宣言ごとに記述してもかまいませんし,複数の宣言を中括弧で囲んでもかまいません.こうすることで,Cで記述されたモジュールとリンクできるようになりますが,その代償として多重定義はできなくなります.ただし,C結合の関数と,C結合ではない関数との多重定義は可能です.ちなみに,extern "C"を付けない場合は「C++結合」になります.C++結合であることを明示的に指定するために,extern "C++"と記述することもできます*5

extern "C"を記述してC結合にすれば,C++から利用する分には問題ありませんが,今度はCでそのヘッダファイルを使えなくなってしまいます.そこで,次のように条件付きコンパイルを行うことで,C++でもCでもヘッダファイルを共有することができるようになります.

#ifdef __cplusplus
extern "C" {
#endif
void f();
#ifdef __cplusplus
}
#endif

ここで,__cplusplusというのは,C++としてコンパイルされるときだけに定義される「あらかじめ定義されたマクロ名」です.現在の標準C++では,__cplusplusマクロは199711Lに定義されます.なお,__STDC__マクロは,標準Cであることを示すマクロ名ですが,C++でも定義されるかどうかが処理系に依存するので,Cかどうかの判別に使用するのは不適切です.

C++のキーワードや予約名は使用しない

C++では,言語仕様が追加された分,多くのキーワードが追加されています.具体的には,次の42個がそうです.中には,Cでもマクロやtypedef名として使われるものも含まれています(andやwchar_tなど).また,inlineはC99でもキーワードになっています.

●C++のキーワード

and and_eq asm bitand bitor bool catch class
compl const_cast delete dynamic_cast explicit export false friend
inline mutable namespace new not not_eq operator or
or_eq private protected public reinterpret_cast static_cast template this
throw true try typeid typename using virtual wchar_t
xor xor_eq

誤ってキーワードを関数名や変数名などに使用すると,確実にコンパイルエラーになります.しかし,予約名を誤って使った場合には,コンパイルエラーになることもあれば,一見何事もないように見えて,潜在的な不具合に繋がることもあります.別のターゲットに移植する際や,コンパイラのバージョンを変えた場合に,初めて不具合が顕在化することもあります.

予約名というのは,コンパイラや標準ライブラリに予約された名前のことです.Cでも,「予約済み識別子」ということで,次のような識別子は使うことができませんでした.

  • アンダースコアで始まるファイル有効範囲の識別子(_abcなど)
  • アンダースコアで始まり,アンダースコアまたは大文字が続く識別子(__abcや_ABCなど)
  • 標準ヘッダをインクルードした場合,そのヘッダで定義される識別子(strcpyなど)

よく,構造体の定義などで,次のようなコードを見かけますが,_ABCは予約済み識別子ですので,CでもC++で使うことができません.といっても,多くの場合は“たまたま”使えてしまうのですが,規格上は未定義の動作ですので,コンパイルできないこともあれば,動作に異常が出ることもあります.少なくとも移植性はありません.

typedef struct _ABC {
     …
} ABC;

C++では,Cの予約済み識別子に加えて,連続する2つのアンダースコアを含む識別子も予約名であり,プログラムで使うことはできません.具体的には,ABC__やA__BCなどです.A___BCのように,アンダースコアが3つ以上連続していても,連続する2つのアンダースコアが含まれることに変わりはありませんから,やはり使用することはできません.

CとC++の非互換性に注意する

C++は,Cに対する完全な上位互換になっているわけではありません.ここでは,ヘッダファイルで一般的に使用する文法に限って,互換性のない表現を紹介します.

関数原型の仮引数リストを省略した際に,Cでは実引数の型や個数のチェックを行わないという意味でしたが,C++では引数なしの意味になります.また,式や関数原型の仮引数の中で,構造体などを定義する書き方もC++ではできません.静的な型チェックがC++では相当強化されていることもあり,マクロで式を定義している場合には,Cでは問題なく使えたものが,C++ではコンパイルできないことがあります.文字リテラルの型がCではint型だったのが,C++ではchar型になっているため,文字リテラルのサイズに依存したコードは誤動作します.

void f(); // ← Cでは,引数の型および個数のチェックなし.C++では,「void f(void);」と同じ意味 
#define alignof(type) \ // ← Cでは,型typeが要求する境界調整を返す.C++ではコンパイルできない 
offsetof(struct { char a; type b; }, b)
int trace(const unsigned char*); 
#define TRACE(s)  trace("TRACE: " ## s)
#define CHAR_SIZE  sizeof('\0')  // ← Cではsizeof(int)と同じ.C++ではsizeof(char)と同じ 

やや現実離れした強引な例も含まれていますが,これらはCとC++の非互換性が問題なる例です.

CとC++の間で互換性のない表現というのは,多くの場合,Cとしても良くないコードであることが多いはずです.あらためて見直すことで,純粋なCのコードとしても,より洗練されるのではないでしょうか.

Cの関数をC++から呼び出す実例

ここまでに解説したテクニックを踏まえて,Cで記述した関数をC++から呼び出すサンプルコードをご紹介します.

【add.c】

// add.c
#include "add.h"
int add(int a, int b)
{
    return a + b;
}

【add.h】

// add.h
#ifndef ADD_H_
#define ADD_H_
#ifdef __cplusplus
extern "C" {
#endif
int add(int, int);
#ifdef __cplusplus
}
#endif
#endif

【main.cpp】

// main.cpp
#include <stdio.h>
#include "add.h"
int main()
{
    int value = add(1, 2);
    printf("%d\n", value);
    return 0;
}

このサンプルでは,Cで記述した2つの整数値a,bを加算した値を返すadd関数を,C++から呼び出しています.CとC++に共通のヘッダファイルadd.hさえ用意すれば,C++からは,Cのときとまったく同じようにadd関数を呼び出すことができます.

このサンプルコードもそうですが,多くの処理系では,ソースファイルの拡張子(あるいは添え字)が「.cpp」の場合にC++としてコンパイルするようになっています.ほかにも,「.cc」や「.c++」などもC++と見なすことが多いようです.処理系によっては,拡張子の変更以外に,何らかのオプションを指定したり,コマンドそのものを変える必要があるものもあります.たとえば,GCCの場合,Cをコンパイルするときはgcc(クロスコンパイラの場合は,arm-elf-gccのようにターゲット名が前に付きます)ですが,C++をコンパイルするときはg++というコマンドを使用します(こちらもクロスコンパイラの場合は,arm-elf-g++のようにターゲット名が前に付きます).

*5 「C結合」や「C++結合」のように,言語を指定した外部結合のことを「言語結合」といいます.必ずサポートされることが補償されているのはCとC++だけですが,処理系によっては,extern "Fortran"やextern "Ada"といった指定を行うこともできます.

1.4.2 C++の関数をCから呼び出すには

先ほどは,Cで実装された関数をC++から呼び出す方法でしたが,今度はその逆で,C++で実装された関数をCから呼び出す方法について説明します.C99さえ考慮しなければ,Cの関数をC++から呼び出すには,ヘッダファイルだけの対応で済みました.しかし,C++の関数をCから呼び出す場合は,ヘッダファイルだけの対応では済まないことが多々あります.先ほどと同じように,C++の関数をCから呼び出すために必要な,工夫すべきポイントを挙げてみます.

  • 外部識別子をすべてC結合にする.
  • C++特有の型を使用しない.
  • 例外を送出しない.
  • クラスの扱い.

それでは,これらの工夫すべき点を順に解説していきます.

外部識別子をC結合にする

Cから呼び出せる関数は,C++でいうところの「C結合」でなければなりません.具体的には,C++側で関数を宣言/定義する際に,extern "C"を付ける必要があるわけです.extern "C"を付けてC結合にするだけで,とりあえずC++側で定義した関数を呼び出すための最低限の条件を整えることができます.C結合にした場合,多重定義ができなくなることにご注意ください.また,名前空間も機能せず,異なる名前空間で宣言されたC結合を持つ同名の関数は,同じものを指すことになります.

C++特有の型を使用しない

C結合にしたとしてもC++特有の型が仮引数並びまたは返却型に使うことはできません.クラスや参照などがそれに当たります.なお,wchar_t型はC++にあってCにはない型ですが,Cでもヘッダなどでtypedef名として定義されているので,それを使えばまず問題はないと思われます.bool型はやや微妙で,C99では_Bool型がサポートされ,ヘッダではboolというマクロが定義されますが,できれば使用は避けたほうが無難です.C++のboolとC99の_Boolの間の互換性は,規格上保証されないことはもちろん,互換性があるだろうと推測することさえ困難だからです.また,列挙型もCとC++ではサイズが異なる可能性があるので,(おそらく大丈夫だとは思いますが)できれば避けたほうが無難です.

例外を送出しない

C++の関数は例外を送出する可能性がありますが,Cから呼び出した際に例外が送出されてもどうすることもできませんので,C++側で関数を定義するときは,内部ですべての例外をとらえて,エラーコードを返すなどの方法に置き換えてやる必要があります.

クラスの扱い

クラス型へのポインタを引数または返却値に使いたい場合,C側では何か別のポインタに置き換える必要があります.C++側でクラスを定義する際,クラスキーにclassではなくstructを使っていれば,そのままC側でも同じ型名で扱うことができます.C++では,structを使ってもクラスを定義することができるのです.具体的には次のようになります.

【C++側】

// C++側
struct A
{
     …
};
extern "C"
int func(A* p)
{
     …
}

【C側】

// C側
struct A; // ← 不完全型の宣言 
int func(struct A* p);

いずれにしても,C++からCの関数を呼び出す場合に比べて,CからC++の関数を呼び出すのはなにかと面倒です.特に,メンバー関数を直接呼び出すことができないので,いったん非メンバー関数でラッピングする必要があるなど,いろいろと手間がかかります.

CからC++の関数の直接呼び出しできるだけ避け,コールバック*6に限定したほうがよいでしょう.

*6 関数へのポインタを渡すことで,別の関数から呼び戻してもらう技法のことです.